Skip to content

feat: explore perspective mechanisms - #320

Merged
CamilleLetavernier merged 9 commits into
ai-firstfrom
issues/283-perspective
Jul 7, 2026
Merged

feat: explore perspective mechanisms#320
CamilleLetavernier merged 9 commits into
ai-firstfrom
issues/283-perspective

Conversation

@CamilleLetavernier

@CamilleLetavernier CamilleLetavernier commented Jul 7, 2026

Copy link
Copy Markdown

What it does

  • Add a new Perspective Service with a Command to switch perspectives
  • Add two perspectives: Theia IDE (Default) and AI First
  • The default perspectives simply shows whichever views are configured to open when no layout data is available
  • The AI First perspective includes the AI Chat in the main area, and Explorer/SCM on the right (left side is reserved for future new views, e.g. AI Sessions)
  • Perspective can optionally hide chrome: menu bars, status bar, collapse areas, ...
    • Menu bar hiding was removed in the last iteration: slightly complex due to multiple implementations (Browser, Electron Native, Electron Custom, MacOS...), and we probably want to "filter" menus rather than remove them entirely anyway
  • When a Perspective defines a default area for a view, that is different from the view's default area, the Perspective "wins" (So a perspective can move an existing view to a different location).
  • By default, perspective are pretty open: they define a default layout with a set of views, but users are still free to reorder them however they like (move views around, close/open views, ...)

How to test

  • F1 > Switch Perspective...
  • Select AI First
  • The new layout should be applied
    • At the moment, open views and editors are never closed; they are only rearranged (follow-up required)
    • New views may be opened if they are not open already
  • F1 > Switch Perspective > AI First (Active)
    • Nothing happens: this is already the active perspective
  • F1 > Switch Perspective > Theia IDE
    • The default perspective is restored in its latest known state

Follow-ups

  • Perspective Contribution improvements: Perspective contribution point #284
  • Perspective Layout persistence: Per-perspective layout persistence #285
  • Handle Hide/Close views: views that are not part of the target perspective should be either closed or hidden/disabled
    • This is a potentially complex issue as we don't want to lose "Transient state". Switching back and forth between perspective should restore the initial state as much as possible (so closing/reopening the view might not be a good solution)
  • Handle disabling areas: currently, perspectives can indicate that an area (left/right/bottom) should be collapsed by default, but they can't prevent users from re-opening them. This is probably related to the Hide/Close views, as this mechanism is only interesting for perspectives that want to really strictly control what should be shown to the user, and how it should be shown (locked-down mode).
  • Handle secondary windows: currently, secondary windows are kept outside of the perspective mechanism. They remain open where they are when switching perspectives. When closing Theia, they are attached back to their preferred location and then persisted in the Layout Data

Breaking changes

  • This PR introduces breaking changes and requires careful review. If yes, the breaking changes section in the changelog has been updated.

Attribution

Review checklist

Reminder for reviewers

@CamilleLetavernier
CamilleLetavernier requested a review from sdirix July 7, 2026 09:05
@CamilleLetavernier
CamilleLetavernier marked this pull request as draft July 7, 2026 09:06
@CamilleLetavernier
CamilleLetavernier changed the base branch from master to ai-first July 7, 2026 09:09
@CamilleLetavernier
CamilleLetavernier marked this pull request as ready for review July 7, 2026 09:09
Comment on lines +190 to +201
protected applyChrome(descriptor: PerspectiveDescriptor): void {
const perspectiveHidesMenu = descriptor.chromeOptions?.hideMenuBar ?? false;
const prefHidesMenu = ['compact', 'hidden'].includes(
this.corePreferences['window.menuBarVisibility']
);
this.shell.topPanel.setHidden(perspectiveHidesMenu || prefHidesMenu);

const perspectiveHidesStatus = descriptor.chromeOptions?.hideStatusBar ?? false;
const prefHidesStatus = !this.preferenceService.get<boolean>(
'workbench.statusBar.visible', true
);
this.statusBar.setHidden(perspectiveHidesStatus || prefHidesStatus);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reimplements chrome handling that already lives in the shell and status bar, and both keep managing the same targets. The menu-bar branch duplicates ApplicationShell.setTopPanelVisibility (

protected setTopPanelVisibility(preference: string): void {
const hiddenPreferences = ['compact', 'hidden'];
this.topPanel.setHidden(hiddenPreferences.includes(preference));
), and the status-bar branch fights StatusBarImpl's own preference listener (
this.preferences.onPreferenceChanged(preference => {
if (preference.preferenceName === 'workbench.statusBar.visible') {
this.setHidden(!this.preferences.get('workbench.statusBar.visible', true));
}
})
). Since the shell also listens to window.menuBarVisibility (
this.setTopPanelVisibility(this.corePreferences['window.menuBarVisibility']);
});
this.corePreferences.onPreferenceChanged(preference => {
if (preference.preferenceName === 'window.menuBarVisibility') {
this.setTopPanelVisibility(this.corePreferences['window.menuBarVisibility']);
), a preference change makes both set topPanel hidden and the result depends on listener order. Better to route through the shell rather than set topPanel/statusBar directly here.

Comment thread packages/ai-ide/src/browser/ai-first-perspective-contribution.ts Outdated
Comment thread packages/core/src/browser/perspective-service.ts Outdated
Comment thread packages/core/src/browser/perspective-service.ts Outdated
Comment thread packages/core/src/browser/shell/view-contribution.ts Outdated

@sdirix sdirix left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comments

Comment thread packages/core/src/browser/perspective-service.ts
Comment thread packages/core/src/browser/perspective-service.ts
Comment thread packages/core/src/browser/perspective-service.ts

setMenuBarHiddenByPerspective(hidden: boolean): void {
this.perspectiveHidesTopPanel = hidden;
this.setTopPanelVisibility(this.corePreferences['window.menuBarVisibility']);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Electron this likely has no effect: the shell only wires top-panel visibility for non-Electron (init guards these setTopPanelVisibility calls with !environment.electron.is(),

if (!environment.electron.is()) {
this.corePreferences.ready.then(() => {
this.setTopPanelVisibility(this.corePreferences['window.menuBarVisibility']);
});
this.corePreferences.onPreferenceChanged(preference => {
if (preference.preferenceName === 'window.menuBarVisibility') {
this.setTopPanelVisibility(this.corePreferences['window.menuBarVisibility']);
) and Electron uses a native menu, so AI First's hideMenuBar won't hide the menu there.

Comment thread packages/core/src/browser/shell/application-shell.ts
import { EXPLORER_VIEW_CONTAINER_ID } from '@theia/navigator/lib/browser';
import { SCM_VIEW_CONTAINER_ID } from '@theia/scm/lib/browser/scm-contribution';

const CHAT_VIEW_WIDGET_ID = 'chat-view-widget';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai-ide already depends on @theia/ai-chat-ui, so this could import ChatViewWidget.ID (

public static ID = 'chat-view-widget';
) instead of hardcoding the string.

@CamilleLetavernier
CamilleLetavernier requested a review from sdirix July 7, 2026 14:51
@CamilleLetavernier

Copy link
Copy Markdown
Author

Thanks for the review!

I updated the PR to address the comments. I removed menu bar visibility option as it's a bit tedious to consistently support, due to the several different options. Also, we likely want to "filter" menus rather than "hide" them entirely.

@sdirix sdirix left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One minor comment. COuld be adressed in a follow up

if (this.switchInProgress) {
await this.switchInProgress;
}
this.switchInProgress = this.doSwitchPerspective(id);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The added guard serializes two switches but not three or more: a third concurrent caller resumes from the same await this.switchInProgress and then reassigns switchInProgress to its own doSwitchPerspective, so it runs concurrently with the second one and their savedLayouts writes can still interleave. Chaining instead would fully serialize, e.g. this.switchInProgress = (this.switchInProgress ?? Promise.resolve()).then(() => this.doSwitchPerspective(id)).

@CamilleLetavernier
CamilleLetavernier requested a review from sdirix July 7, 2026 15:54

const savedLayout = this.savedLayouts.get(id);
if (savedLayout) {
await this.shell.setLayoutData(savedLayout);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

activePerspectiveId is already advanced at L148, but setLayoutData here (and collapsePanel below) aren't guarded like the widget loop is. If either rejects, the service reports the new perspective as active while its layout was never applied, and since switches are chained the rejection also drops any switch queued behind it. A try/catch around the layout application would keep the state consistent. Fine as a follow-up.

@CamilleLetavernier
CamilleLetavernier merged commit 3a42dc2 into ai-first Jul 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants